package pfc.pfcjoc.core;

import static playn.core.PlayN.graphics;
import static playn.core.PlayN.log;

import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.PrintStream;

import java.util.ArrayList;
import java.util.HashMap;

import org.jbox2d.collision.shapes.PolygonShape;
import org.jbox2d.common.Vec2;
import org.jbox2d.dynamics.Body;
import org.jbox2d.dynamics.BodyDef;
import org.jbox2d.dynamics.World;

import pfc.pfcjoc.entities.Background;
import pfc.pfcjoc.entities.Block;
import pfc.pfcjoc.entities.StaticElement;
import pfc.pfcjoc.entities.Element;
import pfc.pfcjoc.entities.EndBlock;
import pfc.pfcjoc.entities.GroundBlock;
import pfc.pfcjoc.entities.Player;
import pfc.pfcjoc.entities.Ramp;
import pfc.pfcjoc.entities.Toad;
import pfc.pfcjoc.entities.Tube;
import pfc.pfcjoc.entities.WoodBlock;

import playn.core.Events;
import playn.core.GroupLayer;
import playn.core.ImmediateLayer;
import playn.core.Json;
import playn.core.Json.Writer;
import playn.core.Layer;
import playn.core.PlayN;
import playn.core.ResourceCallback;
import playn.core.Surface;

public class Level {
  protected World world;
  private GroupLayer worldLayer;

  private Player player;
  private Vec2 playerStartPos;
  protected ArrayList<Element> worldElems = new ArrayList<Element>(0);
  private ArrayList<Body> bodiesToRemove = new ArrayList<Body>(0);
  private ImmediateLayer bgColor;

  private Body ground = null;
  private Body wallLeft = null;
  private Body wallRight = null;
  // TODO: mida del mon a loadlevel
  private static float width = 24 * 2;// 24 = 800 pixels
  private static float height = 18;// 18 = 600 pixels

  private HashMap<Layer, Element> layerToElement;

  private boolean levelLoaded = false;
  private int nElemsLoaded = 0;
  private int nElemsToLoad = 0;
  private ResourceCallback<Boolean> callback;


  public Level(World levelWorld) {
    // Init variables
    world = levelWorld;
    layerToElement = new HashMap<Layer, Element>();
    worldLayer = graphics().createGroupLayer();
    // loadLevel();
  }


  public Element add(Vec2 pos, String elem) {
    Element el = null;
    // TODO: tots els constructors amb paràmetre pos en unitats de pantalla (pixel)
    if (elem.equals("ramp")) {
      el = new Ramp(world, worldLayer, pfcjoc.toPhy(pos));
    }
    else if (elem.equals("bg")) {
      el = new Background(world, worldLayer, pos);
    }
    else if (elem.equals("bush")) {
      el = new StaticElement(world, worldLayer, pos);
    }
    else if (elem.equals("block")) {
      el = new Block(world, worldLayer, pfcjoc.toPhy(pos));
    }
    else if (elem.equals("groundblock")) {
      el = new GroundBlock(world, worldLayer, pfcjoc.toPhy(pos));
    }
    else if (elem.equals("tube")) {
      el = new Tube(world, worldLayer, pfcjoc.toPhy(pos));
    }
    else if (elem.equals("toad")) {
      el = new Toad(world, worldLayer, pfcjoc.toPhy(pos));
    }
    else if (elem.equals("woodblock")) {
      el = new WoodBlock(world, worldLayer, pfcjoc.toPhy(pos));
    }
    else if (elem.equals("endblock")) {
      el = new EndBlock(world, worldLayer, pfcjoc.toPhy(pos));
    }

    if (el == null) {
      log().error("Tipus incorrecte d'element");
    }

    layerToElement.put(el.getLayer(), el);
    worldElems.add(el);
    return el;
  }


  public void remove(Element elem) {
    //temp: no es pot esborrar bloc final
    
    if(elem.getName().equals("endblock")) return;
    // Borrem element si existia
    if (worldElems.remove(elem)) {
      // Borrem la capa
      worldLayer.remove(elem.getLayer());
      // Borrem l'associació capa-Element
      layerToElement.remove(elem.getLayer());
      // Borrem el body si en tenia (només PhysicalElement)
      if (elem.getBody() != null) {
        bodiesToRemove.add(elem.getBody());
      }
      // log().info("mida wordLayer: " + String.valueOf(worldLayer.size()));
      // log().info("mida layerToElement: " + String.valueOf(layerToElement.size()));
      // log().info("mida wordElemslayer: " + String.valueOf(worldElems.size()));
    }
  }


  public void removeAll() {
    for (Element elem : worldElems) {
      // Borrem el body si en tenia (només PhysicalElement)
      Body b = elem.getBody();
      if (b != null) {
        world.destroyBody(b);
      }
      // Borrem la capa
      worldLayer.remove(elem.getLayer());
    }
    worldElems.clear();
    layerToElement.clear();

    // Eliminem el jugador
    player.remove();

    // Eliminem límits del nivell
    if (wallLeft != null) {
      world.destroyBody(wallLeft);
      wallLeft = null;
    }
    if (wallRight != null) {
      world.destroyBody(wallRight);
      wallRight = null;
    }
    if (ground != null) {
      world.destroyBody(ground);
      ground = null;
    }
  }


  public Element getElemAt(Events.Position event) {
    Element el = null;
    Layer.HasSize layer;
    // Player?
    layer = (Layer.HasSize) player.getLayer();
    if (Layer.Util.hitTest(layer, event)) {
      el = player;
    }
    // Element?
    else {
      for (int i = 0; i < worldLayer.size(); i++) {
        layer = (Layer.HasSize) worldLayer.get(i);
        if (Layer.Util.hitTest(layer, event)) {
          el = layerToElement.get(layer);
        }
      }
    }

    return el;
  }


  public void saveLevel() {
    if (PlayN.storage().isPersisted() == false) {
      log().info("Els canvis no es guardaran permanetment");
    }

    // Generem string en format JSON
    Writer writer = PlayN.json().newWriter();
    writer.object();

    writer.value("nivell", 1);

    writer.object("player");
    writer.value("x", (int) pfcjoc.toPix(player.getPosPhy().x));
    writer.value("y", (int) pfcjoc.toPix(player.getPosPhy().y));
    writer.end();

    writer.array("WordlElements");
    for (Element el : worldElems) {
      writer.object();
      writer.value("type", el.getName());
      writer.value("x", (int) pfcjoc.toPix(el.getPosPhy().x));
      writer.value("y", (int) pfcjoc.toPix(el.getPosPhy().y));
      writer.value("depth", el.getLayer().depth());
      writer.end();
    }
    writer.end();

    writer.end();

    String output = writer.write();
    PlayN.storage().setItem("level1", output);

    // Guardem string a disc (nomes sobre JAVA)
    // OutputStream out;
    // PrintStream ps; // declare a print stream object
    // try {
    // // Create a new file output stream
    // out = new FileOutputStream("level1.json");
    //
    // // Connect print stream to the output stream
    // ps = new PrintStream(out);
    //
    // ps.println(output);
    // System.err.println("Write successfully");
    // ps.close();
    // } catch (Exception e) {
    // System.err.println("Error in writing to file");
    // }
  }


  public void loadLevel(final ResourceCallback<Boolean> callback) {
    log().info(" ===== LOAD LEVEL =====");

    this.callback = callback;

    // Color de fons (si no hi ha cap background que el tapi)
    bgColor = graphics().createImmediateLayer(new ImmediateLayer.Renderer() {
      public void render(Surface surf) {
        surf.setFillColor(0xFF3666A8);
        surf.fillRect(0, 0, graphics().width(), graphics().height());
      }
    });
    bgColor.setDepth(-210);
    graphics().rootLayer().add(bgColor);

    String resource = null;
    resource = PlayN.storage().getItem("level1");
    // TODO: oferir llista de nivells per carregar

    // Si s'havia editat i guardat el nivell el carreguem
    if (resource != null) {
      parseLevel(resource);
    }
    // Si no hi havia nivell editat carreguem de disc el nivell per defecte (temporal)
    else {
      PlayN.assets().getText("levels/level1.json", new ResourceCallback<String>() {
        @Override
        public void done(String res) {
          parseLevel(res);
        }


        @Override
        public void error(Throwable err) {
          log().error("Error loading level!", err);
        }
      });
    }
  }


  public void parseLevel(String resource) {
    float x, y;
    Json.Object document = PlayN.json().parse(resource);

    Json.Object jsonPlayer = document.getObject("player");
    x = jsonPlayer.getNumber("x");
    y = jsonPlayer.getNumber("y");
    playerStartPos = new Vec2(x, y);
    player = new Player(world, worldLayer, new Vec2(x, y));
    player.addCallback(new ResourceCallback<Element>() {
      @Override
      public void done(Element elem) {
        nElemsLoaded++;
        checkLoadingProgress();
      }


      @Override
      public void error(Throwable err) {
        log().error("Error loading image!", err);
      }
    });

    Json.Array jsonWorldElems = document.getArray("WordlElements");
    // Num elems a carregar + player
    nElemsToLoad = jsonWorldElems.length() + 1;

    for (int i = 0; i < jsonWorldElems.length(); i++) {
      Json.Object jsonElem = jsonWorldElems.getObject(i);
      String type = jsonElem.getString("type");
      x = (float) jsonElem.getNumber("x");
      y = (float) jsonElem.getNumber("y");
      float depth = (float) jsonElem.getNumber("depth");

      Vec2 pos = new Vec2(x, y);
      Element element = add(pos, type);
      element.getLayer().setDepth(depth);

      element.addCallback(new ResourceCallback<Element>() {
        @Override
        public void done(Element elem) {
          nElemsLoaded++;
          checkLoadingProgress();
        }


        @Override
        public void error(Throwable err) {
          log().error("Error loading image!", err);
        }
      });

    }

  }


  private void checkLoadingProgress() {
    if (nElemsLoaded == nElemsToLoad) {
      calculateWorldLimits();
      levelLoaded = true;
      callback.done(true);
      log().info("LEVEL LOADED: " + String.valueOf(nElemsLoaded) + " Elements");
    }
  }


  private void calculateWorldLimits() {
    float minX = Float.MAX_VALUE;
    float maxX = Float.MIN_VALUE;
    for (Element elem : worldElems) {
      float xElem = elem.getPosPhy().x;
      if ((xElem - pfcjoc.toPhy(elem.getWidth() / 2)) < minX) {
        minX = xElem - pfcjoc.toPhy(elem.getWidth() / 2);
      }
      else if ((xElem + pfcjoc.toPhy(elem.getWidth() / 2)) > maxX) {
        maxX = xElem + pfcjoc.toPhy(elem.getWidth() / 2);
      }
    }

    // World de 0 a maxX(width)
    width = maxX;

    // Límit esq
    wallLeft = world.createBody(new BodyDef());
    PolygonShape wallLeftShape = new PolygonShape();
    Vec2 leftDown = new Vec2(minX, 0);
    Vec2 leftUp = new Vec2(minX, height);
    wallLeftShape.setAsEdge(leftDown, leftUp);
    wallLeft.createFixture(wallLeftShape, 0.0f);

    // Límit dreta
    wallRight = world.createBody(new BodyDef());
    PolygonShape wallRightShape = new PolygonShape();
    Vec2 rightDown = new Vec2(maxX, 0);
    Vec2 rightUp = new Vec2(maxX, height);
    wallRightShape.setAsEdge(rightDown, rightUp);
    wallRight.createFixture(wallRightShape, 0.0f);

    // Ground
    ground = world.createBody(new BodyDef());
    PolygonShape groundShape = new PolygonShape();
    float groundHeight = height + 2 * pfcjoc.toPhy(getPlayer().getHeight());
    groundShape.setAsEdge(new Vec2(0, groundHeight), new Vec2(width, groundHeight));
    ground.createFixture(groundShape, 0.0f);
  }


  public GroupLayer getLevelLayer() {
    return worldLayer;
  }


  public ImmediateLayer getBgColorLayer() {
    return bgColor;
  }


  public float getWidth() {
    return width;
  }


  public float getHeight() {
    return height;
  }


  public Player getPlayer() {
    return player;
  }


  public Vec2 getPlayerStartPos() {
    return playerStartPos;
  }


  public boolean isLoaded() {
    return levelLoaded;
  }


  public void paint(float alpha) {
    // PAINTING
    player.paint(alpha);
    for (Element elem : worldElems) {
      elem.paint(alpha);
    }
  }


  public void update(float delta) {
    // Eliminem bodies marcats per a ser eliminats
    for (Body b : bodiesToRemove) {
      world.destroyBody(b);
    }
    bodiesToRemove.clear();
  }
}
